package com.svndiffdoc;

import java.util.ArrayList;
import java.util.List;

public class FileDiff {
    private static final String STYLE_CLASS_OLD_LINE_EQUAL   = "oldLineEqual";
    private static final String STYLE_CLASS_OLD_LINE_CHANGED = "oldLineChanged";
    private static final String STYLE_CLASS_OLD_LINE_DELETED = "oldLineDeleted";
    private static final String STYLE_CLASS_NEW_LINE_EQUAL   = "newLineEqual";
    private static final String STYLE_CLASS_NEW_LINE_CHANGED = "newLineChanged";
    private static final String STYLE_CLASS_NEW_LINE_ADDED   = "newLineAdded";

    private final List<AnnotatedFileLine> oldFileLines;
    private final List<AnnotatedFileLine> newFileLines;
    private final DiffView diffView;

    public FileDiff(AnnotatedFile oldFile, AnnotatedFile newFile) {
        this.oldFileLines = (oldFile != null ? oldFile.getLines() : new ArrayList<AnnotatedFileLine>());
        this.newFileLines = (newFile != null ? newFile.getLines() : new ArrayList<AnnotatedFileLine>());
        this.diffView = new DiffView();
        generateDiff();
    }

    private void generateDiff() {
        int N = oldFileLines.size();
        int M = newFileLines.size();

        // build diff array from old and new file
        if ((M > 0) && (N > 0)) {

            // opt[i][j] = length of LCS of x[i..M] and y[j..N]
            int[][] opt = new int[M+1][N+1];

            // compute length of LCS and all subproblems via dynamic programming
            for (int i = M-1; i >= 0; i--) {
                for (int j = N-1; j >= 0; j--) {
                    if (newFileLines.get(i).equals(oldFileLines.get(j))) {
                        opt[i][j] = opt[i+1][j+1] + 1;
                    }
                    else {
                        opt[i][j] = Math.max(opt[i+1][j], opt[i][j+1]);
                    }
                }
            }

            // build match point list
            List<int[]> matchPoints = new ArrayList<int[]>();
            int i = 0, j = 0;
            while((i < M) && (j < N)) {
                if (newFileLines.get(i).equals(oldFileLines.get(j))) {
                    matchPoints.add(new int[] {j++, i++});
                }
                else if (opt[i+1][j] >= opt[i][j+1]) {
                    i++;
                }
                else {
                    j++;
                }
            }

            // build diff view from match point list
            int[] prevMatchPoint = new int[] {-1, -1};
            for (int[] matchPoint : matchPoints) {
                addLinesBetweenMatchPoints(prevMatchPoint, matchPoint);
                addMatchPointLine(matchPoint);
                prevMatchPoint = matchPoint;
            }
            addLinesBetweenMatchPoints(prevMatchPoint, new int[] {N, M});
        }

        // build dummy diff array if old file is empty
        else if (M > 0) {
            addLinesBetweenMatchPoints(new int[] {-1, -1}, new int[] {0, M});
        }

        // build dummy diff array if new file is empty
        else if (N > 0) {
            addLinesBetweenMatchPoints(new int[] {-1, -1}, new int[] {N, 0});
        }
    }

    private void addLinesBetweenMatchPoints(int[] prevMatchPoint, int[] matchPoint) {
        int distance = Math.max((matchPoint[0] - prevMatchPoint[0]), (matchPoint[1] - prevMatchPoint[1]));

        for (int i = 1; i < distance; i++) {
            if (((prevMatchPoint[0] + i) < matchPoint[0]) && ((prevMatchPoint[1] + i) < matchPoint[1])) {
                AnnotatedFileLine oldFileLine = oldFileLines.get(prevMatchPoint[0] + i);
                AnnotatedFileLine newFileLine = newFileLines.get(prevMatchPoint[1] + i);
                LineDiff lineDiff = new LineDiff(oldFileLine.getLine(), newFileLine.getLine());

                oldFileLine.setLineClass(STYLE_CLASS_OLD_LINE_CHANGED);
                oldFileLine.setMatchPoint(false);
                oldFileLine.setLineFragments(lineDiff.getOldLineFragments());
                newFileLine.setLineClass(STYLE_CLASS_NEW_LINE_CHANGED);
                newFileLine.setMatchPoint(false);
                newFileLine.setLineFragments(lineDiff.getNewLineFragments());

                diffView.addLine(oldFileLine, newFileLine);
            }
            else if ((prevMatchPoint[0] + i) < matchPoint[0]) {
                AnnotatedFileLine oldFileLine = oldFileLines.get(prevMatchPoint[0] + i);
                oldFileLine.setLineClass(STYLE_CLASS_OLD_LINE_DELETED);
                oldFileLine.setMatchPoint(false);
                diffView.addLine(oldFileLine, null);
            }
            else if ((prevMatchPoint[1] + i) < matchPoint[1]) {
                AnnotatedFileLine newFileLine = newFileLines.get(prevMatchPoint[1] + i);
                newFileLine.setLineClass(STYLE_CLASS_NEW_LINE_ADDED);
                newFileLine.setMatchPoint(false);
                diffView.addLine(null, newFileLine);
            }
        }
    }

    private void addMatchPointLine(int[] matchPoint) {
        AnnotatedFileLine oldFileLine = oldFileLines.get(matchPoint[0]);
        AnnotatedFileLine newFileLine = newFileLines.get(matchPoint[1]);
        oldFileLine.setLineClass(STYLE_CLASS_OLD_LINE_EQUAL);
        newFileLine.setLineClass(STYLE_CLASS_NEW_LINE_EQUAL);
        oldFileLine.setMatchPoint(true);
        newFileLine.setMatchPoint(true);
        diffView.addLine(oldFileLine, newFileLine);
    }

    public DiffView getDiffView() {
        return diffView;
    }

}
